﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Diagnostics;

using FCS.GlobalComponents;

namespace FCS.Communication
{
    class cRIORelay 
    {
        // Stuff we use to track the bollards, pump, and environmental states
        private bool[] _bollardsState = new bool[FountainConstants.BollardCount];
        private bool[] _pumpState = new bool[FountainConstants.PumpCount];
        private bool[] _environState = new bool[FountainConstants.ReportedEnvironmentalStates];

        // UDP network stuff
        static private UdpClient _udpClient;
        private bool _udpClientLastPacketOK;
        private IPEndPoint _udpCRioEndpoint;
        private const int EndPointDefaultPort = 30096;
        private const int LocalDefaultPort = 5151;

        private const string DefaultCRRIOAddress = "fountain-crio.engr.wisc.edu";

        private object _monitor = new object();

        EventLogWriter _eventLogWriter = new EventLogWriter("Fountain Control System");

        /// <summary>
        /// Sets the new CRIO address and updates the sockets.
        /// </summary>
        /// <param name="address"></param>
        public cRIORelay(IPAddress address, int localPort, int endPointPort)
        {
            SetCRIOAddress(address, localPort, endPointPort);
        }

        public cRIORelay()
        {
            // Set up some initial state
            try
            {
                IPHostEntry entry = Dns.GetHostEntry(DefaultCRRIOAddress);
                SetCRIOAddress(entry.AddressList[0], LocalDefaultPort, EndPointDefaultPort);
            }
            catch (SocketException)
            {
                // This typically means we can't resolve the DNS name of the cRIO. This means that we're pretty screwed, so we'll at
                // least fall back to using the IP
                _eventLogWriter.WriteToLog(EventLogEntryType.Warning, "Couldn't resolve the default cRIO address of " + DefaultCRRIOAddress + ", so falling back to a hardcoded IP.");
                SetCRIOAddress(IPAddress.Parse("128.104.196.80"), LocalDefaultPort, EndPointDefaultPort);
            }
        }

        private void SetCRIOAddress(IPAddress address, int localPort, int endPointPort)
        {
            if (_udpClient != null)
                _udpClient.Close();

            if (localPort == endPointPort)
                throw new Exception("Local port and end point port cannot be the same");

            _udpClient = new UdpClient(localPort);
            _udpCRioEndpoint = new IPEndPoint(address, endPointPort);
            BeginAsyncReceive();
        }

        public void Close()
        {
            _udpClient.Close();
            _udpClient = null;
        }

        /// <summary>
        /// Takes a packet and sends it off to the CRIO. Possibly thread safe
        /// </summary>
        public void SendPacket(byte[] packet)
        {
            lock (_monitor)
            {
                try
                {
                    _udpClient.Send(packet, packet.Length, _udpCRioEndpoint);
                    _udpClientLastPacketOK = true;
                }
                catch (SocketException ex)
                {
                    ProcessSocketException(ex);
                }
            }
        }

        /// <summary>
        /// Logs an exception caused due to a socket error when communicating with the cRIO.
        /// </summary>
        /// <param name="ex"></param>
        private void ProcessSocketException(SocketException ex)
        {
            if (_udpClientLastPacketOK)
            {
                string messageFormat = @"Help!

I'm trying to talk to the cRIO, but when I do so I just get this error:

{0}

Help me find my friend! I'll stop complaining about this until we find him.

Thanks,
Enlight Fountain Server";

                EmailModule.SendStatusEmail("HELP! I can't talk to the cRIO!", string.Format(messageFormat, ex.Message));

                // and supress more errors
                _udpClientLastPacketOK = false;
                _eventLogWriter.WriteToLog(EventLogEntryType.Information, "Sent e-mail to Maquina-Status");
            }

            _eventLogWriter.WriteToLog(EventLogEntryType.Error, "The following exception occured while sending a packet to the cRIO:\n\n" + ex.ToString());
        }

        /// <summary>
        /// Starts the asynchronous receive on the UDP client.
        /// </summary>
        private void BeginAsyncReceive()
        {
            // If we have errors, just eat the packets, but if we fail to much then die
            try
            {
                _udpClient.BeginReceive(PacketReceived, _udpClient);
                return;
            }
            catch (SocketException exception)
            {
                ProcessSocketException(exception);
                if (exception.SocketErrorCode == SocketError.ConnectionReset)
                {
                    // this just means the CRIO wasn't listening, so eat it
                }
                else
                    throw;
            }
        }

        private void PacketReceived(IAsyncResult ar)
        {
            IPEndPoint remoteIP = null;
            UdpClient client = (UdpClient)ar.AsyncState;
            byte[] packet;

            try
            {
                packet = client.EndReceive(ar, ref remoteIP);

                if (!_udpClientLastPacketOK)
                    _eventLogWriter.WriteToLog(EventLogEntryType.Information, "Packet received again from cRIO, everything is back to normal.");

                _udpClientLastPacketOK = true;
            }
            catch (ObjectDisposedException)
            {
                // then we're done with that client, so give up
                return;
            }
            catch (SocketException exception)
            {
                ProcessSocketException(exception);
                BeginAsyncReceive();
                return;
            }

            // otherwise we have a good packet, so process
            try
            {
                if (packet.Length != 3)
                    _eventLogWriter.WriteToLog(EventLogEntryType.Warning, "UDP packet received of size " + packet.Length + " rather than the expected size. The packet is being ignored");
                else
                {
                    lock (_monitor)
                    {
                        // check for changed bollards
                        for (int i = 0; i < FountainConstants.BollardCount; i++)
                        {
                            bool state = (packet[0] & (1 << i)) != 0;
                            _bollardsState[i] = state;
                        }

                        // check for changed pumps
                        // the BollardCount + PumpCount is to account for the offset in the packet
                        for (int i = 0; i < FountainConstants.PumpCount; i++)
                        {
                            bool state = (packet[1] & (1 << i)) != 0;
                            _pumpState[i] = state;
                        }

                        // check to see if the manhole is open or the sidewalk water level is ok
                        for (int i = 0; i < FountainConstants.ReportedEnvironmentalStates; i++)
                        {
                            bool state = (packet[2] & (1 << i)) != 0;
                            _environState[i] = state;
                        }
                    }
                }

            }
            finally
            {
                BeginAsyncReceive();
            }
        }

        /// <summary>
        /// Returns the resources retreive from the last packet sent
        /// </summary>
        /// <returns>Retuns the bollars 1-5, pumps 1-4, and environ states (North Disable, South Disable, Sidewalk OK, Manhole Closed) in that order</returns>
        public List<bool[]> GetResources()
        {
            List<bool[]> ret = new List<bool[]>();
            lock (_monitor)
            {
                ret.Add(_bollardsState);
                ret.Add(_pumpState);
                ret.Add(_environState);
            }
            return ret;
        }
    }
}
